﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Web;

namespace ER.Dynamic
{
    public class ObjectBuilder
    {
        private static readonly AssemblyBuilder AssemblyBuilder;
        private static readonly ModuleBuilder ModuleBuilder;
        private static readonly TypeConflictResolver resolver;

        /// <summary> 
        /// Initialize static variables. 
        /// </summary> 
        static ObjectBuilder()
        {
            AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run);

            ModuleBuilder = AssemblyBuilder.DefineDynamicModule("DynamicTypes");
            resolver = new TypeConflictResolver();
            resolver.Bind(AppDomain.CurrentDomain);
        }
        public ObjectBuilder(string name)
        {
            _typeBuilder = ModuleBuilder.DefineType(name,
            TypeAttributes.Public | TypeAttributes.Class);
            _constructorActions = new List<Action<ILGenerator>>();
        }
        public void Emit()
        {
            resolver.AddTypeBuilder(_typeBuilder);
            this.CreateType();
        }
        /// <summary> 
        /// Encapsulated Reflection.Emit TypeBuilder. 
        /// </summary> 
        private readonly TypeBuilder _typeBuilder;

        /// <summary> 
        /// List of IL actions to for the constructor. 
        /// </summary> 
        private readonly IList<Action<ILGenerator>> _constructorActions;

        /// <summary> 
        /// Method used in the getter portion of a property. 
        /// </summary> 
        public MethodInfo GetProperty { get; set; }
        /// <summary> 
        /// Method used in the setter portion of a property. 
        /// </summary> 
        public MethodInfo SetProperty { get; set; }

        /// <summary> 
        /// Gets or sets the inherited base type. 
        /// </summary> 
        public Type BaseType
        {
            get { return _typeBuilder.BaseType; }
            set
            {
                _typeBuilder.SetParent(value);
            }
        }

        /// <summary> 
        /// Adds a property of the specified type. 
        /// </summary> 
        public void AddProperty(string name, Type type,
        object initValue = null, bool hasGetter = true, bool hasSetter = true)
        {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (type == null)
                throw new ArgumentNullException("type");

            var fieldName = "_" + name.Substring(0, 1).ToLowerInvariant() + name.Substring(1);
            var field = AddField(fieldName, type, initValue, readOnly: !hasSetter);

            Action<ILGenerator> getterActions = null;
            Action<ILGenerator> setterActions = null;

            if (hasGetter)
            {
                if (GetProperty != null)
                {
                    //Return result of GetProperty function 
                    getterActions = il =>
                    {
                        //Put this on stack for GetProperty function 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put this on stack for field 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put value of field on stack for GetProperty function 
                        //Consumes this 
                        il.Emit(OpCodes.Ldfld, field);
                        //Put value of name on stack for GetProperty function 
                        il.Emit(OpCodes.Ldstr, name);
                        //Call GetProperty 
                        //Consumes this, field value, name 
                        il.Emit(OpCodes.Call, GetProperty.MakeGenericMethod(type));
                        //Return result of GetProperty function 
                        il.Emit(OpCodes.Ret);
                    };
                }
                else
                {
                    //Return field value 
                    getterActions = il =>
                    {
                        //Put this on stack for field 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put value of field on stack 
                        //Consumes this 
                        il.Emit(OpCodes.Ldfld, field);
                        //Return field value 
                        il.Emit(OpCodes.Ret);

                    };
                }
            }

            if (hasSetter)
            {
                if (SetProperty != null)
                {
                    //Set field using SetProperty method 
                    setterActions = il =>
                    {
                        //Put this on stack for SetProperty method 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put this on stack for field 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put reference to field on stack for SetProperty method 
                        //Consumes this 
                        il.Emit(OpCodes.Ldflda, field);
                        //Put property setter value on stack for SetProperty method 
                        il.Emit(OpCodes.Ldarg_1);
                        //Put value of name on stack for SetProperty method 
                        il.Emit(OpCodes.Ldstr, name);
                        //Call SetProperty 
                        //Consumes this, ref field, value, name 
                        il.Emit(OpCodes.Call, SetProperty.MakeGenericMethod(type));
                        //Return void, SetProperty shouldn't put anything on the stack 
                        il.Emit(OpCodes.Ret);
                    };
                }
                else
                {
                    //Set field value 
                    setterActions = il =>
                    {
                        //Put this on stack for field 
                        il.Emit(OpCodes.Ldarg_0);
                        //Put property setter value on stack for field 
                        il.Emit(OpCodes.Ldarg_1);
                        //Set field 
                        //Consumes this, value 
                        il.Emit(OpCodes.Stfld, field);
                        //Return void, set field does not put anything on the stack 
                        il.Emit(OpCodes.Ret);
                    };
                }
            }
            var property = _typeBuilder.DefineProperty(name,
        PropertyAttributes.None, type, null);

            if (getterActions != null)
            {
                var getter = _typeBuilder.DefineMethod("get_" + name,
                MethodAttributes.Public, type, Type.EmptyTypes);

                getterActions.Invoke(getter.GetILGenerator());

                property.SetGetMethod(getter);
            }

            if (setterActions != null)
            {
                var setter = _typeBuilder.DefineMethod("set_" + name,
                MethodAttributes.Public, null, new[] { type });

                setterActions.Invoke(setter.GetILGenerator());

                property.SetSetMethod(setter);
            }
        }
        /// <summary> 
        /// Adds a field of the specified type. 
        /// </summary> 
        private FieldBuilder AddField(string name, Type type,
        object initValue = null, bool readOnly = false)
        {
            var attributes = FieldAttributes.Private;

            if (readOnly)
                attributes |= FieldAttributes.InitOnly;

            var field = _typeBuilder.DefineField(name, type, attributes);

            if (initValue != null)
            {
                //Verify type of default value 
                if (initValue.GetType() != type)
                    throw new ArgumentException("Type of initValue does not match!");

                //Add constructor action set set default value to field 
                _constructorActions.Add(il =>
                {
                    //Put this on stack for field 
                    il.Emit(OpCodes.Ldarg_0);
                    //Put default value on stack for field 
                    il.PutValue(initValue);
                    //Set field 
                    //Consumes this, value 
                    il.Emit(OpCodes.Stfld, field);
                });
            }

            return field;
        }
        Type _type = null;
        /// <summary> 
        /// Creates a native type of the current object description. 
        /// </summary> 
        public Type CreateType()
        {
            if (_type != null) return _type;
            var constructor =
            _typeBuilder.DefineConstructor(
            MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);

            var constructorIL = constructor.GetILGenerator();

            var baseType = _typeBuilder.BaseType ?? typeof(object);

            var baseTypeConstructor = baseType.GetConstructor(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
            null,
            Type.EmptyTypes,
            null);

            // Call base constructor 
            //Put this on stack for constructor call 
            constructorIL.Emit(OpCodes.Ldarg_0);
            //Call base type constructor
            //Consumes this 
            constructorIL.Emit(OpCodes.Call, baseTypeConstructor);

            //Include constructor actions 
            foreach (var constructorAction in _constructorActions)
            {
                constructorAction.Invoke(constructorIL);
            }

            //Return void 
            constructorIL.Emit(OpCodes.Ret);
            _type = _typeBuilder.CreateType();
            return _type;
        }
        public static implicit operator Type(ObjectBuilder objectBuilder)
        {
            return objectBuilder._typeBuilder;
        }
    }
}